<template>
<div class="viewport" ref="viewport" @scroll="scrollFn">

    <!-- 自制一个滚动条实现和滚动的效果 -->
    <div class="scroll-bar" ref="scrollbar"></div>
    <!-- 渲染滚动列表 -->
    <div class="scroll-list" :style="{transform: `translate3d(0, ${state.offset}px, 0)`}" ref="nodes">
        <div v-for="(item, index) in visibleData" :vid="item.value.id ? item.value.id : index" :key="item.value.id ? item.value.id : index">
            <slot :item="item.value"></slot>
        </div>
    </div>
</div>
</template>

<script>
import throttle from "lodash/throttle"
import {
    onMounted,
    getCurrentInstance,
    toRefs,
    reactive,
    computed,
    onUpdated,
    ref
} from "vue"
export default {
    name: "VirtualList",
    props: {
        size: {
            type: Number,
            required: true
        },
        remain: {
            type: Number,
            required: true
        },
        data: {
            type: Array,
            required: true
        },
        variable: Boolean
    },
    setup(props) {
        const {
            size,
            remain,
            data,
            variable
        } = toRefs(props)

        const state = reactive({
            start: 0, //渲染数据的开始位置
            end: props.remain, // 渲染数据的结束位置
            offset: 0, // 当前可视区域的偏离位置,
            position: []
        })

        const viewport = ref(null)
        const scrollbar = ref(null)
        const nodes = ref(null)

        const cacheList = () => {
            // 缓存每一项的高度和top值
            state.position = data.value.map((item, index) => ({
                height: size.value,
                top: index * size.value,
                bottom: (index + 1) * size.value
            }))
        }

        let instance = null;
        onMounted(function () {
            instance = getCurrentInstance();
            viewport.value.style.height = size.value * remain.value + 'px';
            scrollbar.value.style.height = data.value.length * size.value + 'px';

            // 如果加载完毕，缓存每一项的高度
            // 先将高度未知记录好。等下滚动的时候去渲染页面的时候取真实的DOM高度，来更新缓存的内容
            // 再重新计算高度
            if (variable.value) {
                cacheList()
            }

        })

        onUpdated(function () {
            instance.ctx.$nextTick(function () {
                // 根据当前显示的内容更新缓存的height, bottom, top
                if (!nodes.value) {
                    return;
                }
                nodes.value.children.forEach(node => {
                    let {
                        height
                    } = node.getBoundingClientRect();
                    let id = ~~node.getAttribute('vid');
                    let oldHeight = state.position[id].height;
                    let val = oldHeight - height; // 计算当前的高度是否有变化
                    if (val) {
                        state.position[id].height = height;
                        state.position[id].bottom = state.position[id].bottom - val; // 底部增加
                        // 从当前位置开始往后将后续的所有元素的位置都改变
                        for (let i = id + 1; i < state.position.length; i++) {
                            state.position[i].top = state.position[i - 1].bottom;
                            state.position[i].bottom = state.position[i].bottom - val;
                        }
                    }
                })
                // 只要更新后就会算出滚动条的最新高度
                scrollbar.value.style.height = state.position[state.position.length - 1].bottom + 'px';
            })
        })

        const handleScroll = () => {
            !instance && (instance = getCurrentInstance());
            let scrollTop = viewport.value.scrollTop;

            if (variable.value) {
                // 如果有传递了variable,则要使用二分查找去查找记录
                state.start = getStartIndex(state.position, scrollTop)
                state.end = state.start + remain.value;
                // 设置偏移量
                state.offset = state.position[state.start - prevCount.value] ? state.position[state.start - prevCount.value].top : 0;
            } else {
                // 获取当前应该从第一个位置开始渲染
                state.start = Math.floor(scrollTop / size.value);
                state.end = state.start + remain.value;
                // 让可视区域去调整偏移位置
                // 如果有预留的渲染，应该将这个位置再向上移动当前预留的这么多位置
                state.offset = state.start * size.value - size.value * prevCount.value;
            }
        }

        const scrollFn = throttle(handleScroll, 200, {
            leading: false
        })
        // 前面预留的数据个数
        const prevCount = computed(() => {
            return Math.min(state.start, remain.value >> 1)
        })

        // 后面预留的数据个数
        const nextCount = computed(() => {
            return Math.min(remain.value >> 1, data.value.length - state.end)
        })

        // 计算当前渲染数据: 
        const visibleData = computed(() => {
            let start = state.start - prevCount.value;
            let end = state.end + nextCount.value;
            return data.value.slice(start, end)
        })

        return {
            state,
            visibleData,
            scrollFn,
            viewport,
            scrollbar,
            nodes
        }
    },
}

// 查找当前滚动到的需要找到的值
function getStartIndex(position, value) {
    let start = 0,
        end = position.length - 1,
        temp = null;
    while (start <= end) {
        let middleIndex = (start + end) >> 1;
        let middleValue = position[middleIndex].bottom; // 找到当前中间的那个人的结尾的位置的值
        if (middleValue === value) {
            // 如果找到了，就返回当前的下一个人的是位置
            return middleIndex;
        } else if (middleValue < value) {
            start = middleIndex + 1;
        } else if (middleValue > value) {
            if (temp === null || temp > middleIndex) {
                temp = middleIndex; // 做一个临时的变量，不断的缩小查找的范围，再最终没有找的数据的情况下能够找到一个接近的值
            }
            end = middleIndex - 1;
        }
    }
    return temp;
}
</script>

<style lang="css" scoped>
.viewport {
    overflow-y: scroll;
    position: relative;
    border: 1px solid #eee;
}

.scroll-list {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
}

.scroll-bar {
    width: 100%;
}
</style>
